from flask import Flask, request, session, jsonify, render_template,  redirect, url_for, Response
from sqlalchemy import create_engine, text
import os, time, hmac, csv, io, datetime
from zoneinfo import ZoneInfo
import json

TZ = ZoneInfo(os.getenv("TZ", "Asia/Shanghai"))  # 可用环境变量覆盖时区
def _read_payload():
    """
    更健壮的解析：优先标准 JSON；否则从原始字节解析，
    先按 UTF-8 严格解码，失败或含大量 U+FFFD 时再尝试 GBK 兜底。
    """
    # 1) 标准 application/json 走 Flask 自带解析
    data = request.get_json(silent=True)
    if data is not None:
        return data

    # 2) 原始字节（不要 as_text=True，避免提前用“替换字符”糊掉）
    raw = request.get_data(cache=False)  # bytes
    if raw:
        def try_load(text: str):
            try:
                return json.loads(text)
            except Exception:
                return None

        # 2.1 严格 UTF-8
        try:
            txt = raw.decode("utf-8", errors="strict")
            data = try_load(txt)
            if data is not None:
                # 如果解码成功但出现大量替换符 U+FFFD，说明其实不是 UTF-8
                if txt.count("\ufffd") <= 1:
                    return data
        except UnicodeDecodeError:
            pass

        # 2.2 兜底：GBK（含 GB2312/GB18030 大多兼容）
        try:
            txt_gbk = raw.decode("gbk", errors="strict")
            data = try_load(txt_gbk)
            if data is not None:
                return data
        except UnicodeDecodeError:
            pass

    # 3) 表单
    if request.form:
        return request.form.to_dict()

    # 4) 查询串兜底
    if request.args:
        return request.args.to_dict()

    return None

def _to_iso(ts_raw: int | None) -> str:
    if ts_raw is None:
        return ""
    ts = int(ts_raw)
    # 兼容毫秒时间戳
    if ts > 10**12:
        ts //= 1000
    return datetime.datetime.fromtimestamp(ts, TZ).isoformat()

PG_URL = os.getenv("PG_URL", "postgresql+psycopg://verimake:verinew@127.0.0.1:5432/metrics")
ENGINE = create_engine(PG_URL, pool_size=20, max_overflow=40, pool_recycle=1800, pool_pre_ping=True, future=True)

app = Flask(__name__)
app.config["SECRET_KEY"] = os.getenv("SECRET_KEY", "dev-secret-change-me")
# 管理账号（环境变量配置；没配就用默认示例）
ADMIN_USER = os.getenv("ADMIN_USER", "admin")
ADMIN_PASS = os.getenv("ADMIN_PASS", "VerimakeNew")

def is_admin():
    return session.get("is_admin") is True

def parse_ts(ts_raw, allow_future_skew=300) -> int:
    ts = int(float(ts_raw))
    if ts > 10**10: ts //= 1000
    if ts < 0: raise ValueError("ts<0")
    if ts > int(time.time()) + allow_future_skew: raise ValueError("ts too new")
    return ts


# @app.before_request
# def _dbg_log():
#     print("REQ", request.method, request.path)
#
# @app.errorhandler(401)
# def _handle_401(e):
#     # 首页不再显示那段英文 401 页，直接渲染应用
#     if request.path == "/":
#         return render_template("index.html"), 200
#     return jsonify({"error": "unauthorized"}), 401


@app.before_request
def _guard_only_clear():
    # 只保护清空接口
    if request.path == "/api/clear" and not is_admin():
        return jsonify({"error": "unauthorized"}), 401

# 首页：单页面，根据 is_admin 渲染不同区块
@app.route("/", methods=["GET"])
def index():
    return render_template("index.html", is_admin=is_admin(), login_error=None)

# 登录提交：表单 POST 到 /login，再重定向回 /
@app.route("/login", methods=["GET", "POST"])
def login():
    if request.method == "POST":
        u = request.form.get("username", "")
        p = request.form.get("password", "")
        nxt = request.form.get("next") or url_for("index")
        if hmac.compare_digest(u, ADMIN_USER) and hmac.compare_digest(p, ADMIN_PASS):
            session["is_admin"] = True
            return redirect(nxt)
        return render_template("login.html", error="用户名或密码错误", next=nxt), 401
    # GET
    nxt = request.args.get("next") or url_for("index")
    return render_template("login.html", next=nxt)

# 退出登录
@app.route("/logout", methods=["GET"])
def logout():
    session.clear()
    return redirect(url_for("index"))

@app.route("/api/data", methods=["POST"])
def ingest():
    """
    接收数据并写入（客户端无需 ts）：
    { "id": 1, "username": "alice", "value": 12.34 }
    """
    payload = _read_payload()
    if not payload:
        return jsonify({"error": "缺少请求体(JSON 或 form)"}), 400

    missing = {"id", "value"} - set(payload.keys())
    if missing:
        return jsonify({"error": f"必需字段缺失: {sorted(missing)}"}), 400

    try:
        device_id = int(payload["id"])
        value = float(payload["value"])
        username = payload.get("username")
        now_sec = int(time.time())  # 统一使用服务器时间（秒）
    except Exception as e:
        return jsonify({"error": str(e)}), 400

    # 忽略客户端上传的 ts（如果带了也不使用）
    try:
        with ENGINE.begin() as c:
            c.execute(
                text("""
                    INSERT INTO readings (id, username, value, ts, received_at)
                    VALUES (:id, :u, :v, :ts, :ra)
                """),
                {"id": device_id, "u": username, "v": value, "ts": now_sec, "ra": now_sec},
            )
        return jsonify({"status": "ok"}), 201
    except Exception as e:
        return jsonify({"error": f"DB error: {e}"}), 500

@app.route("/api/latest", methods=["GET"])
def latest_per_id():
    ids_param = request.args.get("ids","").strip()
    where = ""; params = {}
    if ids_param:
        ids = tuple(int(x) for x in ids_param.split(",") if x.strip())
        where = "WHERE r.id = ANY(:ids)"
        params["ids"] = list(ids)  # psycopg 支持数组绑定
    sql = f"""
    SELECT id, username, value, ts, received_at FROM (
      SELECT r.*,
             ROW_NUMBER() OVER (PARTITION BY id ORDER BY ts DESC, received_at DESC, rid DESC) rn
      FROM readings r {where}
    ) t WHERE rn=1 ORDER BY id ASC
    """
    with ENGINE.connect() as c:
        rows = c.execute(text(sql), params).mappings().all()
    return jsonify([dict(r) for r in rows])

@app.route("/api/history/<int:device_id>", methods=["GET"])
def history(device_id:int):
    limit = int(request.args.get("limit", 50)); limit = 50 if limit<=0 or limit>1000 else limit
    with ENGINE.connect() as c:
        rows = c.execute(text("""
            SELECT id, username, value, ts, received_at
            FROM readings WHERE id=:id
            ORDER BY ts DESC, received_at DESC, rid DESC
            LIMIT :lim
        """), {"id": device_id, "lim": limit}).mappings().all()
    return jsonify([dict(r) for r in rows])

# 清空接口：必须已登录
@app.route("/api/clear", methods=["POST"])
def clear_all():
    if not is_admin():
        return jsonify({"error": "unauthorized"}), 401
    try:
        with ENGINE.begin() as c:
            cnt = c.execute(text("SELECT COUNT(*) FROM readings")).scalar_one()
            # 若之前序列 owner 未改到 verimake，可先用不重置自增的 TRUNCATE
            c.execute(text("TRUNCATE TABLE readings RESTART IDENTITY"))
        return jsonify({"status": "ok", "deleted": int(cnt)}), 200
    except Exception as e:
        return jsonify({"error": f"清空失败: {e}"}), 500


@app.route("/api/export", methods=["GET"])
def export_csv():
    if not is_admin():
        return jsonify({"error": "unauthorized"}), 401

    ts = datetime.datetime.now(TZ).strftime("%Y%m%d_%H%M%S")
    filename = f"readings_{ts}.csv"

    def generate():
        sio = io.StringIO()
        writer = csv.writer(sio)
        # 表头：保留原始戳，并新增 ISO 字段
        writer.writerow(["rid", "id", "username", "value",
                         "ts", "ts_iso", "received_at", "received_at_iso"])
        yield sio.getvalue(); sio.seek(0); sio.truncate(0)

        sql = """
            SELECT rid, id, username, value, ts, received_at
            FROM readings
            ORDER BY id ASC, ts ASC, received_at ASC, rid ASC
        """
        with ENGINE.connect() as c:
            result = c.execution_options(stream_results=True).execute(text(sql))
            for row in result:
                writer.writerow([
                    row.rid,
                    row.id,
                    row.username or "",
                    row.value,
                    row.ts,
                    _to_iso(row.ts),
                    row.received_at,
                    _to_iso(row.received_at),
                ])
                yield sio.getvalue(); sio.seek(0); sio.truncate(0)

    headers = {"Content-Disposition": f'attachment; filename="{filename}"'}
    return Response(generate(), mimetype="text/csv; charset=utf-8", headers=headers)


if __name__ == "__main__":
    # 开发环境启动；生产可用 gunicorn/uwsgi
    app.config["JSON_AS_ASCII"] = False
    app.json.ensure_ascii = False
    app.run(host="0.0.0.0", port=5000, debug=True)
